/*
 * Copyright (c) 2024 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { DfuCallback } from './DfuCallback'
import { WorkerMsg, WorkCmd } from './WorkMsg';
import { DfuServiceInitiator } from './DfuServiceInitiator'
import { DfuProgressInfo, ProgressListener } from './DfuProgressInfo'
import { DfuService } from './DfuService'
import { DfuServiceProvider } from './DfuServiceProvider'
import { DfuDeviceSelector } from './DfuDeviceSelector'
import { BaseDfuImpl } from './BaseDfuImpl'
import { DfuDefaultDeviceSelector } from './DfuDefaultDeviceSelector'

import hilog from '@ohos.hilog';
import fs from '@ohos.file.fs';
import ble from '@ohos.bluetooth.ble';
import constant from '@ohos.bluetooth.constant'
import { BusinessError } from '@ohos.base';
import { InputStream } from './internal/InputStream';
import { ArchiveInputStream } from './internal/ArchiveInputStream';
import { HexInputStream } from './internal/HexInputStream'
import { emitter } from '@kit.BasicServicesKit';

export class DfuBaseService implements ProgressListener{

  private TAG: string = "DfuBaseService";
  private DOMAIN: number = 0x8632;

  /* package */ static DEBUG: boolean = false;

  public static NOTIFICATION_ID: number = 283; // a random number
  public static NOTIFICATION_CHANNEL_DFU: string = "dfu";

  /**
   * The address of the device to update.
   */
  public static EXTRA_DEVICE_ADDRESS: string = "no.nordicsemi.android.dfu.extra.EXTRA_DEVICE_ADDRESS";
  /**
   * The optional device name. This name will be shown in the notification.
   */
  public static EXTRA_DEVICE_NAME: string = "no.nordicsemi.android.dfu.extra.EXTRA_DEVICE_NAME";
  /**
   * A boolean indicating whether to disable the progress notification in the status bar.
   * Defaults to false.
   */
  public static EXTRA_DISABLE_NOTIFICATION: string = "no.nordicsemi.android.dfu.extra.EXTRA_DISABLE_NOTIFICATION";
  /**
   * A boolean indicating whether the DFU service should be set as a foreground service.
   * It is recommended to have it as a background service at least on Android Oreo or newer as
   * the background service will be killed by the system few moments after the user closed the
   * foreground app.
   * <p>
   * Read more here: <a href="https://developer.android.com/about/versions/oreo/background.html">https://developer.android.com/about/versions/oreo/background.html</a>
   */
  public static EXTRA_FOREGROUND_SERVICE: string = "no.nordicsemi.android.dfu.extra.EXTRA_FOREGROUND_SERVICE";
  /**
   * An extra private field indicating which reconnection attempt is being performed.
   * In case of error 133 the service will retry to connect 2 more times.
   */
  private static EXTRA_RECONNECTION_ATTEMPT: string = "no.nordicsemi.android.dfu.extra.EXTRA_RECONNECTION_ATTEMPT";
  /**
   * An extra private field indicating which DFU attempt is being performed.
   * If the target device will disconnect for some unknown reason during DFU, the service will
   * retry to connect and continue. In case of Legacy DFU it will reconnect and restart process.
   */
  /* package */ static EXTRA_DFU_ATTEMPT: string = "no.nordicsemi.android.dfu.extra.EXTRA_DFU_ATTEMPT";
  /**
   * Maximum number of DFU attempts. Default value is 0.
   */
  public static EXTRA_MAX_DFU_ATTEMPTS: string = "no.nordicsemi.android.dfu.extra.EXTRA_MAX_DFU_ATTEMPTS";
  /**
   * If the new firmware (application) does not share the bond information with the old one,
   * the bond information is lost. Set this flag to <code>true</code> to make the service create
   * new bond with the new application when the upload is done (and remove the old one).
   * When set to <code>false</code> (default), the DFU service assumes that the LTK is shared
   * between them. Note: currently it is not possible to remove the old bond without creating
   * a new one so if your old application supported bonding while the new one does not you have
   * to modify the source code yourself.
   * <p>
   * In case of updating the soft device the application is always removed together with the
   * bond information.
   * <p>
   * Search for occurrences of EXTRA_RESTORE_BOND in this file to check the implementation and
   * get more details.
   * <p>
   * This flag is ignored when Secure DFU Buttonless Service is used.
   * It will keep or will not restore the bond depending on the Buttonless service type.
   */
  public static EXTRA_RESTORE_BOND: string = "no.nordicsemi.android.dfu.extra.EXTRA_RESTORE_BOND";
  /**
   * This flag indicated whether the bond information should be kept or removed after an upgrade
   * of the Application. If an application is being updated on a bonded device with the DFU
   * Bootloader that has been configured to preserve the bond information for the new application,
   * set it to <code>true</code>.
   * <p>
   * By default the Legacy DFU Bootloader clears the whole application's memory. It may be,
   * however, configured in the \Nordic\nrf51\components\libraries\bootloader_dfu\dfu_types.h
   * file (sdk 11, line 76: <code>#define DFU_APP_DATA_RESERVED 0x0000</code>) to preserve some pages.
   * The BLE_APP_HRM_DFU sample app stores the LTK and System Attributes in the first
   * two pages, so in order to preserve the bond information this value should be changed to
   * 0x0800 or more. For Secure DFU this value is by default set to 3 pages.
   * When those data are preserved, the new Application will notify the app with the
   * Service Changed indication when launched for the first time. Otherwise this service will
   * remove the bond information from the phone and force to refresh the device cache
   * (see {@link #refreshDeviceCache(android.bluetooth.BluetoothGatt, boolean)}).
   * <p>
   * In contrast to {@link #EXTRA_RESTORE_BOND} this flag will not remove the old bonding and
   * recreate a new one, but will keep the bond information untouched.
   * <p>
   * The default value of this flag is <code>false</code>.
   * <p>
   * This flag is ignored when Secure DFU Buttonless Service is used. It will keep or remove the
   * bond depending on the Buttonless service type.
   */
  public static EXTRA_KEEP_BOND: string = "no.nordicsemi.android.dfu.extra.EXTRA_KEEP_BOND";
  /**
   * This property must contain a boolean value.
   * <p>
   * The {@link DfuBaseService}, when connected to a DFU target will check whether it is in
   * application or in DFU bootloader mode. For DFU implementations from SDK 7.0 or newer
   * this is done by reading the value of DFU Version characteristic.
   * If the returned value is equal to 0x0100 (major = 0, minor = 1) it means that we are in the
   * application mode and jump to the bootloader mode is required.
   * <p>
   * However, for DFU implementations from older SDKs, where there was no DFU Version
   * characteristic, the service must guess. If this option is set to false (default) it will count
   * number of device's services. If the count is equal to 3 (Generic Access, Generic Attribute,
   * DFU Service) it will assume that it's in DFU mode. If greater than 3 - in app mode.
   * This guessing may not be always correct. One situation may be when the nRF chip is used to
   * flash update on external MCU using DFU. The DFU procedure may be implemented in the
   * application, which may (and usually does) have more services.
   * In such case set the value of this property to true.
   */
  public static EXTRA_FORCE_DFU: string = "no.nordicsemi.android.dfu.extra.EXTRA_FORCE_DFU";

  /**
   * This flag indicates whether the service should scan for bootloader in Legacy DFU after
   * switching using buttonless service. The default value is false.
   */
  public static EXTRA_FORCE_SCANNING_FOR_BOOTLOADER_IN_LEGACY_DFU: string = "no.nordicsemi.android.dfu.extra.EXTRA_FORCE_SCANNING_FOR_BOOTLOADER_IN_LEGACY_DFU";
  /**
   * This options allows to disable the resume feature in Secure DFU. When the extra value is set
   * to true, the DFU will send Init Packet and Data again, despite the firmware might have been
   * send partially before. By default, without setting this extra, or by setting it to false,
   * the DFU will resume the previously cancelled upload if CRC values match.
   * <p>
   * It is ignored when Legacy DFU is used.
   * <p>
   * This feature seems to help in some cases:
   * <a href="https://github.com/NordicSemiconductor/Android-DFU-Library/issues/71">#71</a>.
   */
  public static EXTRA_DISABLE_RESUME: string = "no.nordicsemi.android.dfu.extra.EXTRA_DISABLE_RESUME";
  /**
   * The MBR size.
   *
   * @see DfuServiceInitiator#setMbrSize(int)
   */
  public static EXTRA_MBR_SIZE: string = "no.nordicsemi.android.dfu.extra.EXTRA_MBR_SIZE";
  /**
   * This extra allows you to control the MTU that will be requested (on Lollipop or newer devices).
   * If the field is null, the service will not request higher MTU and will use MTU = 23
   * (even if it has been set to a higher value before).
   */
  public static EXTRA_MTU: string = "no.nordicsemi.android.dfu.extra.EXTRA_MTU";
  /**
   * This extra value will be used when MTU request returned with an error. That means, that
   * MTU has been requested before and may not be changed again. This value will be used instead.
   */
  public static EXTRA_CURRENT_MTU: string = "no.nordicsemi.android.dfu.extra.EXTRA_CURRENT_MTU";
  /**
   * Set this flag to true to enable experimental buttonless feature in Secure DFU from SDK 12.
   * When the experimental Buttonless DFU Service is found on a device, the service will use it to
   * switch the device to the bootloader mode, connect to it in that mode and proceed with DFU.
   * <p>
   * <b>Please, read the information below before setting it to true.</b>
   * <p>
   * In the SDK 12.x the Buttonless DFU feature for Secure DFU was experimental.
   * It is NOT recommended to use it: it was not properly tested, had implementation bugs
   * (e.g. <a href="https://devzone.nordicsemi.com/question/100609/sdk-12-bootloader-erased-after-programming/">this thread</a>)
   * and does not require encryption and therefore may lead to DOS attack (anyone can use it
   * to switch the device to bootloader mode). However, as there is no other way to trigger
   * bootloader mode on devices without a button, this DFU Library supports this service,
   * but the feature must be explicitly enabled here.
   * Be aware, that setting this flag to false will not protect your devices from this kind of
   * attacks, as an attacker may use another app for that purpose. To be sure your device is
   * secure remove this experimental service from your device.
   * <p>
   * <b>Spec:</b><br>
   * Buttonless DFU Service UUID: 8E400001-F315-4F60-9FB8-838830DAEA50<br>
   * Buttonless DFU characteristic UUID: 8E400001-F315-4F60-9FB8-838830DAEA50 (the same)<br>
   * Enter Bootloader Op Code: 0x01<br>
   * Correct return value: 0x20-01-01 , where:<br>
   * 0x20 - Response Op Code<br>
   * 0x01 - Request Code<br>
   * 0x01 - Success<br>
   * The device should disconnect and restart in DFU mode after sending the notification.
   * <p>
   * In SDK 14 this issue was fixed by Buttonless Service With Bonds.
   */
  public static EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU: string = "no.nordicsemi.android.dfu.extra.EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU";
  /**
   * The duration of a delay that will be added before sending each data packet in Secure DFU,
   * in milliseconds. This defaults to 0 for backwards compatibility reason.
   */
  public static EXTRA_DATA_OBJECT_DELAY: string = "no.nordicsemi.android.dfu.extra.EXTRA_DATA_OBJECT_DELAY";
  /**
   * This property must contain a boolean value.
   * <p>
   * If true the Packet Receipt Notification procedure will be enabled.
   * See DFU documentation on <a href="http://infocenter.nordicsemi.com">Infocenter</a> for more details.
   * The number of packets before receiving a Packet Receipt Notification is set with property
   * {@link #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE}.
   * The PRNs by default are enabled on devices running Android 4.3, 4.4.x and 5.x and
   * disabled on 6.x and newer.
   *
   * @see #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE
   */
  public static EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED: string = "no.nordicsemi.android.dfu.extra.EXTRA_PRN_ENABLED";
  /**
   * This property must contain a positive integer value, usually from range 1-200.
   * <p>
   * The default value is {@link DfuServiceInitiator#DEFAULT_PRN_VALUE}.
   * Setting it to 0 will disable the Packet Receipt Notification procedure.
   * When sending a firmware using the DFU procedure the service will send this number of packets
   * before waiting for a notification. Packet Receipt Notifications are used to synchronize
   * the sender with receiver.
   * <p>
   * On Android, calling
   * {@link android.bluetooth.BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic)}
   * simply adds the packet to outgoing queue before returning the callback. Adding the next
   * packet in the callback is much faster than the real transmission (also the speed depends on
   * the device chip manufacturer) and the queue may reach its limit. When does, the transmission
   * stops and Android Bluetooth hangs (see Note below). Using PRN procedure eliminates this
   * problem as the notification is send when all packets were delivered the queue is empty.
   * <p>
   * Note: this bug has been fixed on Android 6.0 Marshmallow and now no notifications are required.
   * The onCharacteristicWrite callback will be postponed until half of the queue is empty and
   * upload will be resumed automatically. Disabling PRNs speeds up the upload process on those
   * devices.
   *
   * @see #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED
   */
  public static EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE: string = "no.nordicsemi.android.dfu.extra.EXTRA_PRN_VALUE";
  /**
   * A path to the file with the new firmware. It may point to a HEX, BIN or a ZIP file.
   * Some file manager applications return the path as a String while other return a Uri.
   * Use the {@link #EXTRA_FILE_URI} in the later case. For files included
   * in /res/raw resource directory please use {@link #EXTRA_FILE_RES_ID} instead.
   */
  public static EXTRA_FILE_PATH: string = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_PATH";
  /**
   * See {@link #EXTRA_FILE_PATH} for details.
   */
  public static EXTRA_FILE_URI: string = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_URI";
  /**
   * See {@link #EXTRA_FILE_PATH} for details.
   */
  public static EXTRA_FILE_RES_ID: string = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_RES_ID";
  /**
   * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+).
   * Must point to a 'dat' file corresponding with the selected firmware.
   * The Init packet may contain just the CRC (in case of older versions of DFU) or the
   * Extended Init Packet in binary format (SDK 7.0+).
   */
  public static EXTRA_INIT_FILE_PATH: string = "no.nordicsemi.android.dfu.extra.EXTRA_INIT_FILE_PATH";
  /**
   * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+).
   * Must point to a 'dat' file corresponding with the selected firmware.
   * The Init packet may contain just the CRC (in case of older versions of DFU) or the
   * Extended Init Packet in binary format (SDK 7.0+).
   */
  public static EXTRA_INIT_FILE_URI: string = "no.nordicsemi.android.dfu.extra.EXTRA_INIT_FILE_URI";
  /**
   * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+).
   * Must point to a 'dat' file corresponding with the selected firmware.
   * The Init packet may contain just the CRC (in case of older versions of DFU) or the
   * Extended Init Packet in binary format (SDK 7.0+).
   */
  public static EXTRA_INIT_FILE_RES_ID: string = "no.nordicsemi.android.dfu.extra.EXTRA_INIT_FILE_RES_ID";
  /**
   * The input file mime-type. Currently only "application/zip" (ZIP) or "application/octet-stream"
   * (HEX or BIN) are supported. If this parameter is empty the "application/octet-stream" is assumed.
   */
  public static EXTRA_FILE_MIME_TYPE: string = "no.nordicsemi.android.dfu.extra.EXTRA_MIME_TYPE";
  // Since the DFU Library version 0.5 both HEX and BIN files are supported.
  // As both files have the same MIME TYPE the distinction is made based on the file extension.
  public static MIME_TYPE_OCTET_STREAM: string = "application/octet-stream";
  public static MIME_TYPE_ZIP: string = "application/zip";
  /**
   * This optional extra parameter may contain a file type. Currently supported are:
   * <ul>
   * <li>{@link #TYPE_SOFT_DEVICE} - only Soft Device update</li>
   * <li>{@link #TYPE_BOOTLOADER} - only Bootloader update</li>
   * <li>{@link #TYPE_APPLICATION} - only application update</li>
   * <li>{@link #TYPE_AUTO} - the file is a ZIP file that may contain more than one HEX/BIN + DAT files.
   * Since SDK 8.0 the ZIP Distribution packet is a recommended way of delivering firmware files.
   * Please, see the DFU documentation for more details. A ZIP distribution packet may be created
   * using the 'nrf util' Python application, available at
   * <a href="https://github.com/NordicSemiconductor/pc-nrfutil">https://github.com/NordicSemiconductor/pc-nrfutil</a>.
   * The ZIP file MAY contain only the following files: <b>softdevice.hex/bin</b>,
   * <b>bootloader.hex/bin</b>, <b>application.hex/bin</b> to determine the type based on its name.
   * At lease one of them MUST be present.
   * </li>
   * </ul>
   * If this parameter is not provided the type is assumed as follows:
   * <ol>
   * <li>If the {@link #EXTRA_FILE_MIME_TYPE} field is <code>null</code> or is equal to
   * {@value #MIME_TYPE_OCTET_STREAM} - the {@link #TYPE_APPLICATION} is assumed.</li>
   * <li>If the {@link #EXTRA_FILE_MIME_TYPE} field is equal to {@value #MIME_TYPE_ZIP}
   * - the {@link #TYPE_AUTO} is assumed.</li>
   * </ol>
   */
  public static EXTRA_FILE_TYPE: string = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_TYPE";
  /**
   * <p>
   * The file contains a new version of Soft Device.
   * <p>
   * Since DFU Library 7.0 all firmware may contain an Init packet. The Init packet is required
   * if Extended Init Packet is used by the DFU bootloader (SDK 7.0+)..
   * The Init packet for the bootloader must be placed in the .dat file.
   *
   * @see #EXTRA_FILE_TYPE
   */
  public static TYPE_SOFT_DEVICE: number = 1;
  /**
   * <p>
   * The file contains a new version of Bootloader.
   * <p>
   * Since DFU Library 7.0 all firmware may contain an Init packet. The Init packet is required
   * if Extended Init Packet is used by the DFU bootloader (SDK 7.0+).
   * The Init packet for the bootloader must be placed in the .dat file.
   *
   * @see #EXTRA_FILE_TYPE
   */
  public static TYPE_BOOTLOADER: number = 1 << 1;
  /**
   * <p>
   * The file contains a new version of Application.
   * <p>
   * Since DFU Library 0.5 all firmware may contain an Init packet. The Init packet is required
   * if Extended Init Packet is used by the DFU bootloader (SDK 7.0+).
   * The Init packet for the application must be placed in the .dat file.
   *
   * @see #EXTRA_FILE_TYPE
   */
  public static TYPE_APPLICATION: number = 1 << 2;
  /**
   * <p>
   * A ZIP file that consists of more than 1 file. Since SDK 8.0 the ZIP Distribution packet is
   * a recommended way of delivering firmware files. Please, see the DFU documentation for
   * more details. A ZIP distribution packet may be created using the 'nrf utility' command line
   * application, that is a part of Master Control Panel 3.8.0.
   * For backwards compatibility this library supports also ZIP files without the manifest file.
   * Instead they must follow the fixed naming convention:
   * The names of files in the ZIP must be: <b>softdevice.hex</b> (or .bin), <b>bootloader.hex</b>
   * (or .bin), <b>application.hex</b> (or .bin) in order to be read correctly. Using the
   * Soft Device v7.0.0+ the Soft Device and Bootloader may be updated and sent together.
   * In case of additional application file included, the service will try to send Soft Device,
   * Bootloader and Application together (which is not supported currently) and if it fails,
   * send first SD+BL, reconnect and send the application in the following connection.
   * <p>
   * Since the DFU Library 0.5 you may specify the Init packet, that will be send prior to the
   * firmware. The init packet contains some verification data, like a device type and revision,
   * application version or a list of supported Soft Devices. The Init packet is required if
   * Extended Init Packet is used by the DFU bootloader (SDK 7.0+).
   * In case of using the compatibility ZIP files the Init packet for the Soft Device and Bootloader
   * must be in the 'system.dat' file while for the application in the 'application.dat' file
   * (included in the ZIP). The CRC in the 'system.dat' must be a CRC of both BIN contents if
   * both a Soft Device and a Bootloader is present.
   *
   * @see #EXTRA_FILE_TYPE
   */
  public static TYPE_AUTO: number = 0x00;
  /**
   * An extra field with progress and error information used in broadcast events.
   */
  public static EXTRA_DATA: string = "no.nordicsemi.android.dfu.extra.EXTRA_DATA";
  /**
   * An extra field to send the progress or error information in the DFU notification.
   * The value may contain:
   * <ul>
   * <li>Value 0 - 100 - percentage progress value</li>
   * <li>One of the following status constants:
   * <ul>
   * <li>{@link #PROGRESS_CONNECTING}</li>
   * <li>{@link #PROGRESS_STARTING}</li>
   * <li>{@link #PROGRESS_ENABLING_DFU_MODE}</li>
   * <li>{@link #PROGRESS_VALIDATING}</li>
   * <li>{@link #PROGRESS_DISCONNECTING}</li>
   * <li>{@link #PROGRESS_COMPLETED}</li>
   * <li>{@link #PROGRESS_ABORTED}</li>
   * </ul>
   * </li>
   * <li>An error code with {@link #ERROR_MASK} if initialization error occurred</li>
   * <li>An error code with {@link #ERROR_REMOTE_MASK} if remote DFU target returned an error</li>
   * <li>An error code with {@link #ERROR_CONNECTION_MASK} if connection error occurred
   * (e.g. GATT error (133) or Internal GATT Error (129))</li>
   * </ul>
   * To check if error occurred use:<br>
   * {@code boolean error = progressValue >= DfuBaseService.ERROR_MASK;}
   */
  public static EXTRA_PROGRESS: string = "no.nordicsemi.android.dfu.extra.EXTRA_PROGRESS";
  /**
   * The number of currently transferred part. The SoftDevice and Bootloader may be send
   * together as one part. If user wants to upload them together with an application it has to be
   * sent in another connection as the second part.
   *
   * @see no.nordicsemi.android.dfu.DfuBaseService#EXTRA_PARTS_TOTAL
   */
  public static EXTRA_PART_CURRENT: string = "no.nordicsemi.android.dfu.extra.EXTRA_PART_CURRENT";
  /**
   * Number of parts in total.
   *
   * @see no.nordicsemi.android.dfu.DfuBaseService#EXTRA_PART_CURRENT
   */
  public static EXTRA_PARTS_TOTAL: string = "no.nordicsemi.android.dfu.extra.EXTRA_PARTS_TOTAL";
  /**
   * The current upload speed in bytes/millisecond.
   */
  public static EXTRA_SPEED_B_PER_MS: string = "no.nordicsemi.android.dfu.extra.EXTRA_SPEED_B_PER_MS";
  /**
   * The average upload speed in bytes/millisecond for the current part.
   */
  public static EXTRA_AVG_SPEED_B_PER_MS: string = "no.nordicsemi.android.dfu.extra.EXTRA_AVG_SPEED_B_PER_MS";
  /**
   * The broadcast message contains the following extras:
   * <ul>
   * <li>{@link #EXTRA_DATA} - the progress value (percentage 0-100) or:
   * <ul>
   * <li>{@link #PROGRESS_CONNECTING}</li>
   * <li>{@link #PROGRESS_STARTING}</li>
   * <li>{@link #PROGRESS_ENABLING_DFU_MODE}</li>
   * <li>{@link #PROGRESS_VALIDATING}</li>
   * <li>{@link #PROGRESS_DISCONNECTING}</li>
   * <li>{@link #PROGRESS_COMPLETED}</li>
   * <li>{@link #PROGRESS_ABORTED}</li>
   * </ul>
   * </li>
   * <li>{@link #EXTRA_DEVICE_ADDRESS} - the target device address</li>
   * <li>{@link #EXTRA_PART_CURRENT} - the number of currently transmitted part</li>
   * <li>{@link #EXTRA_PARTS_TOTAL} - total number of parts that are being sent, e.g. if a ZIP
   * file contains a Soft Device, a Bootloader and an Application, the SoftDevice and Bootloader
   * will be send together as one part. Then the service will disconnect and reconnect to the
   * new Bootloader and send the application as part number two.</li>
   * <li>{@link #EXTRA_SPEED_B_PER_MS} - current speed in bytes/millisecond as float</li>
   * <li>{@link #EXTRA_AVG_SPEED_B_PER_MS} - the average transmission speed in bytes/millisecond
   * as float</li>
   * </ul>
   */
  public static BROADCAST_PROGRESS: string = "no.nordicsemi.android.dfu.broadcast.BROADCAST_PROGRESS";
  /**
   * Service is connecting to the remote DFU target.
   */
  public static PROGRESS_CONNECTING: number = -1;
  /**
   * Service is enabling notifications and starting transmission.
   */
  public static PROGRESS_STARTING: number = -2;
  /**
   * Service has triggered a switch to bootloader mode. Now the service waits for the link loss
   * event (this may take up to several seconds) and will connect again to the same device,
   * now started in the bootloader mode.
   */
  public static PROGRESS_ENABLING_DFU_MODE: number = -3;
  /**
   * Service is sending validation request to the remote DFU target.
   */
  public static PROGRESS_VALIDATING: number = -4;
  /**
   * Service is disconnecting from the DFU target.
   */
  public static PROGRESS_DISCONNECTING: number = -5;
  /**
   * The connection is successful.
   */
  public static PROGRESS_COMPLETED: number = -6;
  /**
   * The upload has been aborted. Previous software version will be restored on the target.
   */
  public static PROGRESS_ABORTED: number = -7;
  /**
   * The broadcast error message contains the following extras:
   * <ul>
   * <li>{@link #EXTRA_DATA} - the error number. Use {@link GattError#parse(int)} to get String
   * representation.</li>
   * <li>{@link #EXTRA_DEVICE_ADDRESS} - the target device address</li>
   * </ul>
   */
  public static BROADCAST_ERROR: string = "no.nordicsemi.android.dfu.broadcast.BROADCAST_ERROR";
  /**
   * The type of the error. This extra contains information about that kind of error has occurred.
   * Connection state errors and other errors may share the same numbers. For example, the
   * {@link BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)}
   * method may return a status code 8 (GATT INSUF AUTHORIZATION), while the status code 8
   * returned by {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)}
   * is a GATT CONN TIMEOUT error.
   */
  public static EXTRA_ERROR_TYPE: string = "no.nordicsemi.android.dfu.extra.EXTRA_ERROR_TYPE";
  public static ERROR_TYPE_OTHER: number = 0;
  public static ERROR_TYPE_COMMUNICATION_STATE: number = 1;
  public static ERROR_TYPE_COMMUNICATION: number = 2;
  public static ERROR_TYPE_DFU_REMOTE: number = 3;
  /**
   * If this bit is set than the progress value indicates an error. Use {@link GattError#parse(int)}
   * to obtain error name.
   */
  public static ERROR_MASK: number = 0x1000;
  public static ERROR_DEVICE_DISCONNECTED: number = DfuBaseService.ERROR_MASK; // | 0x00;
  public static ERROR_FILE_NOT_FOUND: number = DfuBaseService.ERROR_MASK | 0x01;
  /**
   * Thrown if service was unable to open the file ({@link java.io.IOException} has been thrown).
   */
  public static ERROR_FILE_ERROR: number = DfuBaseService.ERROR_MASK | 0x02;
  /**
   * Thrown when input file is not a valid HEX or ZIP file.
   */
  public static ERROR_FILE_INVALID: number = DfuBaseService.ERROR_MASK | 0x03;
  /**
   * Thrown when {@link java.io.IOException} occurred when reading from file.
   */
  public static ERROR_FILE_IO_EXCEPTION: number = DfuBaseService.ERROR_MASK | 0x04;
  /**
   * Error thrown when {@code gatt.discoverServices();} returns false.
   */
  public static ERROR_SERVICE_DISCOVERY_NOT_STARTED: number = DfuBaseService.ERROR_MASK | 0x05;
  /**
   * Thrown when the service discovery has finished but the DFU service has not been found.
   * The device does not support DFU of is not in DFU mode.
   */
  public static ERROR_SERVICE_NOT_FOUND: number = DfuBaseService.ERROR_MASK | 0x06;
  /**
   * Thrown when unknown response has been obtained from the target. The DFU target must follow
   * specification.
   */
  public static ERROR_INVALID_RESPONSE: number = DfuBaseService.ERROR_MASK | 0x08;
  /**
   * Thrown when the the service does not support given type or mime-type.
   */
  public static ERROR_FILE_TYPE_UNSUPPORTED: number = DfuBaseService.ERROR_MASK | 0x09;
  /**
   * Thrown when the the Bluetooth adapter is disabled.
   */
  public static ERROR_BLUETOOTH_DISABLED = DfuBaseService.ERROR_MASK | 0x0A;
  /**
   * DFU Bootloader version 0.6+ requires sending the Init packet. If such bootloader version is
   * detected, but the init packet has not been set this error is thrown.
   */
  public static ERROR_INIT_PACKET_REQUIRED: number = DfuBaseService.ERROR_MASK | 0x0B;
  /**
   * Thrown when the firmware file is not word-aligned. The firmware size must be dividable by
   * 4 bytes.
   */
  public static ERROR_FILE_SIZE_INVALID: number = DfuBaseService.ERROR_MASK | 0x0C;
  /**
   * Thrown when the received CRC does not match with the calculated one. The service will try
   * 3 times to send the data, and if the CRC fails each time this error will be thrown.
   */
  public static ERROR_CRC_ERROR: number = DfuBaseService.ERROR_MASK | 0x0D;
  /**
   * Thrown when device had to be paired before the DFU process was started.
   */
  public static ERROR_DEVICE_NOT_BONDED: number = DfuBaseService.ERROR_MASK | 0x0E;
  /**
   * Thrown when the DFU library lost track of what is going on. Reported number of bytes is
   * not equal to the number of bytes sent and due to some other events the library cannot recover.
   * <p>
   * Check <a href="https://github.com/NordicSemiconductor/Android-DFU-Library/issues/229">Issue 229</a>
   */
  public static ERROR_PROGRESS_LOST: number = DfuBaseService.ERROR_MASK | 0x0F;
  /**
   * Flag set when the DFU target returned a DFU error. Look for DFU specification to get error
   * codes. The error code is binary OR-ed with one of: {@link #ERROR_REMOTE_TYPE_LEGACY},
   * {@link #ERROR_REMOTE_TYPE_SECURE} or {@link #ERROR_REMOTE_TYPE_SECURE_EXTENDED}.
   */
  public static ERROR_REMOTE_MASK: number = 0x2000;
  public static ERROR_REMOTE_TYPE_LEGACY: number = 0x0100;
  public static ERROR_REMOTE_TYPE_SECURE: number = 0x0200;
  public static ERROR_REMOTE_TYPE_SECURE_EXTENDED: number = 0x0400;
  public static ERROR_REMOTE_TYPE_SECURE_BUTTONLESS: number = 0x0800;
  /**
   * The flag set when one of {@link android.bluetooth.BluetoothGattCallback} methods was called
   * with status other than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}.
   */
  public static ERROR_CONNECTION_MASK: number = 0x4000;
  /**
   * The flag set when the
   * {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
   * method was called with status other than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}.
   */
  public static ERROR_CONNECTION_STATE_MASK: number = 0x8000;
  /**
   * The log events are only broadcast when there is no nRF Logger installed.
   * The broadcast contains 2 extras:
   * <ul>
   * <li>{@link #EXTRA_LOG_LEVEL} - The log level, one of following: {@link #LOG_LEVEL_DEBUG},
   * {@link #LOG_LEVEL_VERBOSE}, {@link #LOG_LEVEL_INFO}, {@link #LOG_LEVEL_APPLICATION},
   * {@link #LOG_LEVEL_WARNING}, {@link #LOG_LEVEL_ERROR}</li>
   * <li>{@link #EXTRA_LOG_MESSAGE} - The log message</li>
   * </ul>
   */
  public static BROADCAST_LOG: string = "no.nordicsemi.android.dfu.broadcast.BROADCAST_LOG";
  public static EXTRA_LOG_MESSAGE: string = "no.nordicsemi.android.dfu.extra.EXTRA_LOG_INFO";
  public static EXTRA_LOG_LEVEL: string = "no.nordicsemi.android.dfu.extra.EXTRA_LOG_LEVEL";
  /*
   * Note:
   * The nRF Logger API library has been excluded from the DfuLibrary.
   * All log events are now being sent using local broadcasts and may be logged into nRF Logger
   * in the app module. This is to make the Dfu module independent from logging tool.
   *
   * The log levels below are equal to log levels in nRF Logger API library, v 2.0.
   * @see https://github.com/NordicSemiconductor/nRF-Logger-API
   */
  /**
   * Level used just for debugging purposes. It has lowest level
   */
  public static LOG_LEVEL_DEBUG: number = 0;
  /**
   * Log entries with minor importance
   */
  public static LOG_LEVEL_VERBOSE: number = 1;
  /**
   * Default logging level for important entries
   */
  public static LOG_LEVEL_INFO: number = 5;
  /**
   * Log entries level for applications
   */
  public static LOG_LEVEL_APPLICATION: number = 10;
  /**
   * Log entries with high importance
   */
  public static LOG_LEVEL_WARNING: number = 15;
  /**
   * Log entries with very high importance, like errors
   */
  public static LOG_LEVEL_ERROR: number = 20;
  /**
   * Activity may broadcast this broadcast in order to pause, resume or abort DFU process.
   * Use {@link #EXTRA_ACTION} extra to pass the action.
   */
  public static BROADCAST_ACTION = "no.nordicsemi.android.dfu.broadcast.BROADCAST_ACTION";
  /**
   * The action extra. It may have one of the following values: {@link #ACTION_PAUSE},
   * {@link #ACTION_RESUME}, {@link #ACTION_ABORT}.
   */
  public static EXTRA_ACTION = "no.nordicsemi.android.dfu.extra.EXTRA_ACTION";
  /**
   * Pauses the upload. The service will wait for broadcasts with the action set to
   * {@link #ACTION_RESUME} or {@link #ACTION_ABORT}.
   */
  public static ACTION_PAUSE: number = 0;
  /** Resumes the upload that has been paused before using {@link #ACTION_PAUSE}. */
  public static ACTION_RESUME: number = 1;
  /**
   * Aborts the upload. The service does not need to be paused before.
   * After sending {@link #BROADCAST_ACTION} with extra {@link #EXTRA_ACTION} set to this value
   * the DFU bootloader will restore the old application (if there was already an application).
   * Be aware, that uploading the Soft Device will erase the application in order to make space
   * in the memory. In case there is no application, or the application has been removed, the
   * DFU bootloader will be started and user may try to send the application again.
   * The bootloader may advertise with the address incremented by 1 to prevent caching services.
   */
  public static ACTION_ABORT: number = 2;

  public static EXTRA_SCAN_DELAY: string = "no.nordicsemi.android.dfu.extra.EXTRA_SCAN_DELAY";
  public static EXTRA_SCAN_TIMEOUT: string = "no.nordicsemi.android.dfu.extra.EXTRA_SCAN_TIMEOUT";

  public static EXTRA_CUSTOM_UUIDS_FOR_LEGACY_DFU: string = "no.nordicsemi.android.dfu.extra.EXTRA_CUSTOM_UUIDS_FOR_LEGACY_DFU";
  public static EXTRA_CUSTOM_UUIDS_FOR_SECURE_DFU: string = "no.nordicsemi.android.dfu.extra.EXTRA_CUSTOM_UUIDS_FOR_SECURE_DFU";
  public static EXTRA_CUSTOM_UUIDS_FOR_EXPERIMENTAL_BUTTONLESS_DFU: string = "no.nordicsemi.android.dfu.extra.EXTRA_CUSTOM_UUIDS_FOR_EXPERIMENTAL_BUTTONLESS_DFU";
  public static EXTRA_CUSTOM_UUIDS_FOR_BUTTONLESS_DFU_WITHOUT_BOND_SHARING: string = "no.nordicsemi.android.dfu.extra.EXTRA_CUSTOM_UUIDS_FOR_BUTTONLESS_DFU_WITHOUT_BOND_SHARING";
  public static EXTRA_CUSTOM_UUIDS_FOR_BUTTONLESS_DFU_WITH_BOND_SHARING: string = "no.nordicsemi.android.dfu.extra.EXTRA_CUSTOM_UUIDS_FOR_BUTTONLESS_DFU_WITH_BOND_SHARING";

  /**
   * Lock used in synchronization purposes
   */
  // private final Object mLock = new Object();
  // private BluetoothAdapter mBluetoothAdapter;
  private mDeviceAddress: string;
  private mDeviceName: string;
  private mDisableNotification: boolean;
  /**
   * The current connection state. If its value is > 0 than an error has occurred.
   * Error number is a negative value of mConnectionState
   */
  protected mConnectionState: number;
  protected static STATE_DISCONNECTED: number = 0;
  protected static STATE_CONNECTING: number = -1;
  protected static STATE_CONNECTED: number = -2;
  protected static STATE_CONNECTED_AND_READY: number = -3; // indicates that services were discovered
  protected static STATE_DISCONNECTING: number = -4;
  protected static STATE_CLOSED: number = -5;
  /**
   * The number of the last error that has occurred or 0 if there was no error
   */
  private mError: number;
  /**
   * Stores the last progress percent. Used to prevent from sending progress notifications with
   * the same value.
   */
  private mLastProgress: number = -1;
  /* package */
  public mProgressInfo: DfuProgressInfo;
  private mLastNotificationTime: number;

  /** Flag set to true if sending was aborted. */
  private mAborted: boolean;

  private mDfuServiceImpl: DfuCallback;
  private mCurrentDfuImpl: BaseDfuImpl;
  private mFirmwareInputStream: InputStream;
  private mInitFileInputStream: InputStream;
  private gattClient: ble.GattClientDevice;

  public wmsg: WorkerMsg;

  constructor() {
    this.onCreate();
  }

  public onCreate() {
    emitter.on("dfuaction", (eventData: emitter.EventData) => {
      hilog.info(this.DOMAIN, this.TAG, `on dfuaction: ${JSON.stringify(eventData)}`);

      let action: number = eventData.data.action;
      switch (action) {
        case DfuBaseService.ACTION_PAUSE:
          {
            this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "[Broadcast] Pause action received");
            if (this.mDfuServiceImpl != null) {
              this.mDfuServiceImpl.pause();
            }
          }
          break;
          break;
        case DfuBaseService.ACTION_RESUME:
          {
            this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "[Broadcast] Resume action received");
            if (this.mDfuServiceImpl != null) {
              this.mDfuServiceImpl.resume();
            }
          }
          break;
        case DfuBaseService.ACTION_ABORT:
          {
            this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "[Broadcast] Abort action received");
            this.mAborted = true;
            if (this.mDfuServiceImpl != null) {
              this.mDfuServiceImpl.abort();
            }
          }
          break;
      }
    })
  }

  private initialize(): boolean {
    return true;
  }

  public getDeviceSelector(): DfuDeviceSelector{
    return new DfuDefaultDeviceSelector();
  }

  public sendLogBroadcast(level: number, message: string) {
    hilog.debug(this.DOMAIN, this.TAG, message);
    let edata: emitter.EventData = {
      data: {
        "address": this.mDeviceAddress,
        "level": level,
        "log": message,
      }
    }
    emitter.emit("dfulog", edata);
  }

  public async waitUntilDisconnected(gatt: ble.GattClientDevice): Promise<void> {
    let presolve: ((value: void | PromiseLike<void>) => void) | null = null;
    let that = this;
    gatt.on("BLEConnectionStateChange", (state: ble.BLEConnectionChangeState) => {
      hilog.info(that.DOMAIN, that.TAG, `BLEConnectionStateChange: ${JSON.stringify(state)}`);
      if (state.state === constant.ProfileConnectionState.STATE_DISCONNECTED) {
        gatt.off("BLEConnectionStateChange");
        presolve();
      }
    })

    return new Promise(resolve => {
      presolve = resolve;
      gatt.disconnect()
    });
  }

  public waitFor(millis: number): Promise<void> {
    return new Promise(resolve => {
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "wait(" + millis + ")");
      setTimeout(() => {
        resolve();
      }, millis); // 2000 毫秒等于 2 秒
    });
  }

  public refreshDeviceCache(gatt: ble.GattClientDevice, force: boolean) {
    hilog.info(this.DOMAIN, this.TAG, `refreshDeviceCache`);
  }

  private async bleConnectedCB(state: ble.BLEConnectionChangeState) {
    hilog.info(this.DOMAIN, this.TAG, `BLEConnectionStateChange MAC:${state.deviceId}, STATE:${state.state}`);
    if (state.state === constant.ProfileConnectionState.STATE_CONNECTED) {
      hilog.info(this.DOMAIN, this.TAG, `getServices: start: ${this.gattClient}`);
      this.mConnectionState = DfuBaseService.STATE_CONNECTED;
      this.gattClient.getDeviceName((err: BusinessError, data: string) => {
        hilog.info(this.DOMAIN, this.TAG, `GetDevName: ${data}}`)
      })
      this.gattClient.getServices((code: BusinessError, gattServices: Array<ble.GattService>) => {
        hilog.info(this.DOMAIN, this.TAG, `getServicesRes:${JSON.stringify(code)},${JSON.stringify(gattServices)}`)
      })

      let dfuService: BaseDfuImpl = null;
      try {
        /*
         * Device services were discovered. Based on them we may now choose the implementation.
         */
        let serviceProvider: DfuServiceProvider = new DfuServiceProvider();
        this.mDfuServiceImpl = serviceProvider; // This is required if the provider is now able read data from the device
        this.mDfuServiceImpl = dfuService = await serviceProvider.getServiceImpl(this, this.gattClient);
        this.mCurrentDfuImpl = dfuService;
        if (dfuService == null) {
          hilog.warn(this.DOMAIN, this.TAG, "DFU Service not found.");
          this.terminateConnection(this.gattClient, DfuBaseService.ERROR_SERVICE_NOT_FOUND);
          return;
        }

        // Begin the DFU depending on the implementation
        hilog.info(this.DOMAIN, this.TAG, "dfuService.initialize");
        if (dfuService.initialize(this.gattClient, this.wmsg.fileType,
          this.mFirmwareInputStream, this.mInitFileInputStream)) {
          hilog.info(this.DOMAIN, this.TAG, "dfuService.performDfu");
          dfuService.performDfu();
        }
      } catch (e) {
        hilog.error(this.DOMAIN, this.TAG, `onHandle bleConnectedCB err: ${JSON.stringify(e)}`)
        this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload aborted");
        this.terminateConnection(this.gattClient, 0);
        this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_ABORTED);
        this.report(DfuBaseService.ERROR_DEVICE_DISCONNECTED);
      }
    } else if (state.state === constant.ProfileConnectionState.STATE_DISCONNECTED) {
      hilog.info(this.DOMAIN, this.TAG, 'PostMessage: WC_DISCONNECT');
      this.mConnectionState = DfuBaseService.STATE_DISCONNECTED;
      this.mCurrentDfuImpl.finalize(false, this.mCurrentDfuImpl.shouldScanForBootloader());
      this.wmsg.workerPort.postMessage(WorkCmd.WC_DISCONNECT);
    }
  }

  private connect(address: string): ble.GattClientDevice {
    hilog.info(this.DOMAIN, this.TAG, `connect : ${address}`)
    this.mConnectionState = DfuBaseService.STATE_CONNECTING;
    this.gattClient = ble.createGattClientDevice(address);

    //TODO: dump peer devname;
    // this.gattClient.getDeviceName((err: BusinessError, data: string) => {
    //   hilog.info(this.DOMAIN, this.TAG, `GetDevName: ${data}`)
    // })

    // this.gattClient.on("BLEConnectionStateChange", this.bleConnectedCB)
    let that = this;
    this.gattClient.on("BLEConnectionStateChange", (state: ble.BLEConnectionChangeState) => {
      that.bleConnectedCB(state);
      if (state.state === constant.ProfileConnectionState.STATE_CONNECTED) {
        that.gattClient.off("BLEConnectionStateChange");
      }
    })
    this.gattClient.connect();
    return this.gattClient;
  }

  private disconnect(gatt: ble.GattClientDevice) {
    hilog.info(this.DOMAIN, this.TAG, `disconnect : ${this.mDeviceAddress}`);
    if (this.mConnectionState == DfuBaseService.STATE_DISCONNECTED) {
      hilog.error(this.DOMAIN, this.TAG, 'Already Disconnected!');
      return;
    }

    this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Disconnecting...");
    this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_DISCONNECTING);

    gatt.disconnect();
    gatt.close();
    this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Disconnected");
  }

  /**
   * Closes the GATT device and cleans up.
   *
   * @param gatt the GATT device to be closed.
   */
  public close(gatt: ble.GattClientDevice) {
    hilog.info(this.DOMAIN, this.TAG, "Cleaning up...");
    // Call disconnect() to make sure all resources are released. The device should already be
    // disconnected, but that's OK.
    this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.disconnect()");
    gatt.disconnect();
    this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.close()");
    gatt.close();
    this.mConnectionState = DfuBaseService.STATE_CLOSED;
  }

  private sendProgressBroadcast(info: DfuProgressInfo) {
    let data: emitter.EventData = {
      data: {
        "address": this.mDeviceAddress,
        "action": DfuBaseService.BROADCAST_PROGRESS,
        "progress": info.getProgress(),
        "speed": info.getSpeed(),
        "avgSpeed": info.getAverageSpeed(),
        "currentPart": info.getCurrentPart(),
        "partsTotal": info.getTotalParts(),
        "error": 0,
        "errorType": 0,
      }
    }
    emitter.emit("dfuprogress", data);
  }

  private sendErrorBroadcast(error: number) {
    let errorCode: number = 0;
    let errorType: number = 0;
    if ((error & DfuBaseService.ERROR_CONNECTION_MASK) > 0) {
      errorCode = (error & ~DfuBaseService.ERROR_CONNECTION_MASK);
      errorType = DfuBaseService.ERROR_TYPE_COMMUNICATION;
    } else if ((error & DfuBaseService.ERROR_CONNECTION_STATE_MASK) > 0) {
      errorCode = error & ~DfuBaseService.ERROR_CONNECTION_STATE_MASK;
      errorType = DfuBaseService.ERROR_TYPE_COMMUNICATION_STATE;
    } else if ((error & DfuBaseService.ERROR_REMOTE_MASK) > 0) {
      errorCode = error & ~DfuBaseService.ERROR_REMOTE_MASK;
      errorType = DfuBaseService.ERROR_TYPE_DFU_REMOTE;
    } else {
      errorCode = error;
      errorType = DfuBaseService.ERROR_TYPE_OTHER;
    }

    let data: emitter.EventData = {
      data: {
        "address": this.mDeviceAddress,
        "action": DfuBaseService.BROADCAST_ERROR,

        "error": errorCode,
        "errorType": errorType,
      }
    }
    emitter.emit("dfuprogress", data);
  }

  public updateProgressNotification() {
    let info: DfuProgressInfo = this.mProgressInfo;
    let progress: number = info.getProgress();
    if (this.mLastProgress == progress) {
      return;
    }

    this.mLastProgress = progress;

    // send progress or error broadcast
    this.sendProgressBroadcast(info);

    if (this.mDisableNotification) {
      return;
    }

    // the notification may not be refreshed too quickly as the ABORT button becomes not clickable
    // If new state is an end-state, update regardless so it will not stick around in "Disconnecting" state
    let now: number = Date.now();
    if (now - this.mLastNotificationTime < 250 && !(DfuBaseService.PROGRESS_COMPLETED == progress
      || DfuBaseService.PROGRESS_ABORTED == progress)) {
      return;
    }
    this.mLastNotificationTime = now;

    // create or update notification:
    let deviceAddress: string = this.mDeviceAddress;
    let deviceName: string = (this.mDeviceName != null ? this.mDeviceName : 'unknown');


    // Any additional configuration?
    //updateProgressNotification(builder, progress);
  }

  private report(error: number) {
    this.sendErrorBroadcast(error);
  }

  public terminateConnection(gatt: ble.GattClientDevice, error: number) {
    hilog.info(this.DOMAIN, this.TAG, `terminateConnection : ${this.mDeviceAddress}`);
    if (this.mConnectionState != DfuBaseService.STATE_DISCONNECTED) {
      // Disconnect from the device
      this.disconnect(gatt);
    }
    if (error) {
      hilog.error(this.DOMAIN, this.TAG, `terminateConnection err: ${error}`);
      this.report(error);
    }
  }

  private openInputStream(filePath: string, mimeType: string, mbrSize: number, types: number, wmsg: WorkerMsg): InputStream {
    let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
    if (DfuBaseService.MIME_TYPE_ZIP === mimeType) {
      return new ArchiveInputStream(file, mbrSize, types, wmsg);
    }
    if (filePath.toLowerCase().endsWith('hex')) {
      return new HexInputStream(file, mbrSize);
    }
    return null;
  }

  public async onHandle(wmsg: WorkerMsg) {
    const deviceAddress: string = wmsg.deviceAddress;
    const deviceName: string = wmsg.deviceName;
    const disableNotification: boolean = wmsg.disableNotification;
    // const foregroundService: boolean = wmsg.foregroundService;
    const filePath: string = wmsg.filePath;
    const fileUri: string = wmsg.fileUri;
    const fileResId: number = wmsg.fileResId;
    const initFilePath: string = wmsg.initFilePath;
    const initFileUri: string = wmsg.initFileUri;
    const initFileResId: number = wmsg.initFileResId;
    let fileType: number = wmsg.fileType;
    let mimeType: string = wmsg.mimeType;
    this.wmsg = wmsg;

    if (filePath != null && fileType == DfuBaseService.TYPE_AUTO)
      fileType = filePath.toLowerCase().endsWith("zip") ? DfuBaseService.TYPE_AUTO : DfuBaseService.TYPE_APPLICATION;

    mimeType = mimeType != null ? mimeType :
      (fileType == DfuBaseService.TYPE_AUTO ? DfuBaseService.MIME_TYPE_ZIP : DfuBaseService.MIME_TYPE_OCTET_STREAM);

    // Some validation
    if (deviceAddress == null || deviceAddress.length < 17 || (filePath == null && fileUri == null && fileResId == 0)) {
      hilog.warn(this.DOMAIN, this.TAG,
        "Device Address of firmware location are empty. Hint: use DfuServiceInitiator to start DFU");
      return;
    }
    // Check file type and mime-type
    if ((fileType &
      ~(DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER | DfuBaseService.TYPE_APPLICATION)) > 0 ||
      !(DfuBaseService.MIME_TYPE_ZIP === mimeType ||
      DfuBaseService.MIME_TYPE_OCTET_STREAM === mimeType)) {
      hilog.warn(this.DOMAIN, this.TAG, "File type or file mime-type not supported");
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "File type or file mime-type not supported");
      this.report(DfuBaseService.ERROR_FILE_TYPE_UNSUPPORTED);
      return;
    }
    if (DfuBaseService.MIME_TYPE_OCTET_STREAM === mimeType &&
      fileType != DfuBaseService.TYPE_SOFT_DEVICE &&
      fileType != DfuBaseService.TYPE_BOOTLOADER &&
      fileType != DfuBaseService.TYPE_APPLICATION) {
      hilog.warn(this.DOMAIN, this.TAG, "Unable to determine file type");
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Unable to determine file type");
      this.report(DfuBaseService.ERROR_FILE_TYPE_UNSUPPORTED);
      return;
    }

    // TODO: startForeground
    this.mDeviceAddress = deviceAddress;
    this.mDeviceName = deviceName;
    this.mDisableNotification = disableNotification;
    this.mConnectionState = DfuBaseService.STATE_DISCONNECTED;
    this.mError = 0;

    let mbrSize: number = DfuServiceInitiator.DEFAULT_MBR_SIZE;
    let is: InputStream = this.mFirmwareInputStream;
    let initIs: InputStream = this.mInitFileInputStream;

    let firstRun: boolean = this.mFirmwareInputStream == null;

    try {
      // Prepare data to send, calculate stream size
      if (firstRun) {
        //TODO: The files are opened only once, when DFU service is first started.
        //TODO: If need send notification
        hilog.info(this.DOMAIN, this.TAG, 'Opening file...');
        this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Opening file...");
        if (fileUri != null) {
          is = this.openInputStream(fileUri, mimeType, mbrSize, fileType, wmsg);
        } else if (filePath != null) {
          hilog.error(this.DOMAIN, this.TAG, 'Open filePath not support ...');
        } else if (fileResId > 0) {
          hilog.error(this.DOMAIN, this.TAG, 'Open fileResId not support ...');
        }

        //TODO: The Init file Input Stream is kept global only
        //TODO: If ZIP file was given with DAT file(s) inside it will be taken from the ZIP
        if (initFileUri != null) {
          hilog.error(this.DOMAIN, this.TAG, `Open initFileUri not support ${initFileUri}...`);
        } else if (initFilePath != null) {
          hilog.error(this.DOMAIN, this.TAG, `Open initFilePath not support ${initFilePath}...`);
        } else if (initFileResId > 0) {
          hilog.error(this.DOMAIN, this.TAG, `Open initFileResId not support ${initFileResId}...`);
        }

        let imageSizeInBytes: number = is.available();
        if ((imageSizeInBytes % 4) != 0) {
          hilog.error(this.DOMAIN, this.TAG, `The new firmware is not word-aligned.`);
        }
      }

      // Update the file type bit field basing on the ZIP content
      hilog.info(this.DOMAIN, this.TAG, `mimeType = ${mimeType}`);
      if (DfuBaseService.MIME_TYPE_ZIP === mimeType) {
        let zhis: ArchiveInputStream = is as ArchiveInputStream;
        if (fileType == DfuBaseService.TYPE_AUTO) {
          fileType = zhis.getContentType();
        } else {
          fileType = zhis.setContentType(fileType);
        }

        // Validate sizes
        if ((fileType & DfuBaseService.TYPE_APPLICATION) > 0 && (wmsg.applicationSize % 4) != 0)
          throw new Error("Application firmware is not word-aligned.");
        if ((fileType & DfuBaseService.TYPE_BOOTLOADER) > 0 && (wmsg.bootloaderSize % 4) != 0)
          throw new Error("Bootloader firmware is not word-aligned.");
        if ((fileType & DfuBaseService.TYPE_SOFT_DEVICE) > 0 && (wmsg.softDeviceSize % 4) != 0)
          throw new Error("Soft Device firmware is not word-aligned.");

        if (fileType === DfuBaseService.TYPE_APPLICATION) {
          if (zhis.getApplicationInit() != null) {
            initIs = new InputStream(zhis.getApplicationInit());
          }
        } else {
          if (zhis.getSystemInit() != null)
            initIs = new InputStream(zhis.getSystemInit());
        }
      }

      this.mFirmwareInputStream = is;
      this.mInitFileInputStream = initIs;
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Firmware file opened successfully");
    } catch (e) {
      hilog.error(this.DOMAIN, this.TAG, `Error: ${JSON.stringify(e)}`)
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, `Error: ${JSON.stringify(e)}`);
      this.report(DfuBaseService.ERROR_FILE_ERROR);
      return;
    }

    this.mProgressInfo = new DfuProgressInfo(this);
    if (this.mAborted) {
      hilog.warn(this.DOMAIN, this.TAG, "Upload aborted");
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload aborted");
      this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_ABORTED);
      return;
    }

    this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Connecting to DFU target...");
    this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_CONNECTING);

    let before: number = Date.now();
    let gatt: ble.GattClientDevice = this.connect(deviceAddress);
    let after: number = Date.now();
    // Are we connected?
    if (gatt == null) {
      hilog.error(this.DOMAIN, this.TAG, "Bluetooth adapter disabled");
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, "Bluetooth adapter disabled");
      this.report(DfuBaseService.ERROR_BLUETOOTH_DISABLED);
      return;
    }
    hilog.info(this.DOMAIN, this.TAG, `Gatt connected : ${deviceAddress}`)

    //TODO: mError > 0 try attempts

    if (this.mConnectionState == DfuBaseService.STATE_DISCONNECTED) {
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, "Disconnected");
      hilog.error(this.DOMAIN, this.TAG, `mConnectionState : ${this.mConnectionState}}`)
      this.terminateConnection(gatt, DfuBaseService.ERROR_DEVICE_DISCONNECTED);
      return;
    }

    if (this.mAborted) {
      hilog.error(this.DOMAIN, this.TAG, `Upload aborted`)
      this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload aborted");
      this.terminateConnection(gatt, 0);
      this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_ABORTED);
      return;
    }

    this.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Services discovered");
    return;

  }
}
